package org.nativejpa;

import org.nativejpa.query.QuerySynchronization;
import org.nativejpa.query.SQLQuery;
import org.nativejpa.query.NamedSQLTemplate;
import org.nativejpa.query.QueryExt;
import org.nativejpa.query.NamedQueryNotFoundException;
import org.nativejpa.query.SQLTemplate;
import org.nativejpa.query.GeneratedNamedSQLTemplate;
import org.nativejpa.support.sqlgen.SQLGenerator;
import org.nativejpa.mapping.EntityFactory;
import org.nativejpa.mapping.EntityRepository;
import org.nativejpa.mapping.ColumnMetadata;
import org.nativejpa.mapping.MappingEntity;

import javax.persistence.EntityTransaction;
import javax.persistence.Query;
import javax.persistence.FlushModeType;
import javax.persistence.LockModeType;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceException;
import javax.persistence.OptimisticLockException;
import javax.sql.DataSource;
import java.util.List;
import java.util.ArrayList;
import java.sql.Timestamp;

import org.apache.commons.logging.LogFactory;
import org.apache.commons.logging.Log;

/**
 * @author <a href="mailto:jfox.young@gmail.com">Young Yang</a>
 * @create 2009-3-17 16:16:57
 */
public abstract class EntityManagerExt implements EntityManager, QuerySynchronization {

    private static Log log = LogFactory.getLog(EntityManagerExt.class);

    private EntityManagerFactoryImpl emFactory = null;

    private List<QuerySynchronization> synchs = new ArrayList<QuerySynchronization>();

    private boolean closed = false;

    protected EntityManagerExt(EntityManagerFactoryImpl emFactory) {
        this.emFactory = emFactory;
    }

    public EntityManagerFactoryImpl getEntityManagerFactory() {
        return emFactory;
    }
    
    /**
     * register Query synchronization for interceptor around execute
     */
    public void registerQuerySynchronization(QuerySynchronization synchronization) {
        synchs.add(synchronization);
    }

    public DataSource getDataSource() {
        return getEntityManagerFactory().getDataSource();
    }

    public String getUnitName() {
        return getEntityManagerFactory().getUnitName();
    }

    public void beforeExecuteQuery(SQLQuery query) {
        for (QuerySynchronization sync : synchs) {
            sync.beforeExecuteQuery(query);
        }
    }

    public void beforeExecuteUpdate(SQLQuery query) {
        for (QuerySynchronization sync : synchs) {
            sync.beforeExecuteUpdate(query);
        }
    }

    public void afterExecuteQuery(SQLQuery query, boolean success) {
        for (int i = synchs.size() - 1; i >= 0; i--) {
            synchs.get(i).afterExecuteQuery(query, success);
        }
    }

    public void afterExecuteUpdate(SQLQuery query, boolean success) {
        for (int i = synchs.size() - 1; i >= 0; i--) {
            synchs.get(i).afterExecuteUpdate(query, success);
        }
    }

    public abstract EntityTransaction getTransaction();

    public abstract void joinTransaction();

    //TOOD: Resource will be enlist automatically by current Transaction default
    // suspendTransaction try to suspend current thread transaction if it is not bould in this EntityManager, it is important for operating DB cross EntityManager when out of EJB Container
    public abstract void suspendTransaction();

    // resume Transaction after query.execute
    public abstract void resumeTransaction();

    public Query createNamedNativeQuery(String name) {
        checkClosed();
        return createNamedQuery(name);
    }

    /**
     * 用于创建自动生成SQL的Query时调用，指定 partitionName 为了Cache的互操作
     */
    public QueryExt createTempNamedNativeQuery(String name, String sqlString, Class resultClass, String resultsetMapping, String cacheName) {
        checkClosed();
        NamedSQLTemplate namedSQLTemplate = new NamedSQLTemplate(name, sqlString, resultClass, resultsetMapping, null, cacheName);
        return new SQLQuery(this, namedSQLTemplate);
    }

    public QueryExt createNamedQuery(String name) {
        checkClosed();
        NamedSQLTemplate sqlTemplate = getEntityManagerFactory().getNamedQuery(name);
        // 没有对应的 SQLTemplate
        if (sqlTemplate == null) {
            throw new NamedQueryNotFoundException(name);
        }
        return new SQLQuery(this, sqlTemplate);
    }

    public Query createNativeQuery(String sqlString) {
        checkClosed();
        return createNativeQuery(sqlString, MappingEntity.class);
    }

    public Query createNativeQuery(String sqlString, Class resultClass) {
        checkClosed();
        SQLTemplate sqlTemplate = new SQLTemplate(sqlString, resultClass, "");
        return new SQLQuery(this, sqlTemplate);
    }
    
    public void close() {
        checkClosed();
        //TODO: check if EM has active Queries
        closed = true;
    }

    public void clear() {
        checkClosed();
        getEntityManagerFactory().clearCache();
    }

    public boolean isOpen() {
        return !closed;
    }

    protected void checkClosed() {
        if (closed) {
            throw new IllegalStateException("EntityManager has been closed.");
        }
    }

    public boolean contains(Object entity) {
        checkClosed();
        throw new UnsupportedOperationException("EntityManager.contains(Object entity).");
    }

    public Query createNativeQuery(String sqlString, String resultSetMapping) {
        checkClosed();
        //TODO: check the name is acceptable
        String tempName = "TEMP_NATIVE_QUERY_RESULTSET_MAPPING_" + sqlString;
        return createTempNamedNativeQuery(tempName, sqlString, void.class, resultSetMapping, null);
    }

    public Query createQuery(String sqlString) {
        checkClosed();
        //TODO: check the name is acceptable
        log.warn("You use EntityManager.createQuery(" + sqlString + "), will treat as NativeQuery.");
        String tempName = "TEMP_QUERY_" + sqlString;
        return createTempNamedNativeQuery(tempName, sqlString, void.class, null, null); // treat as NativeQuery
    }

    public void flush() {
        checkClosed();
        // do nothing :-)
    }

    public Object getDelegate() {
        checkClosed();
        return this;
    }

    // implement by creating auto select sql
    public <T> T find(Class<T> entityClass, Object primaryKey) {
        checkClosed();
        Query query = new SQLQuery(this, GeneratedNamedSQLTemplate.createSelectNamedSQLTemplate(entityClass));
        ColumnMetadata columnMetadata = EntityRepository.getInstance().getPKColumnMetadata(entityClass);
        String colName = columnMetadata.getName();
        T entity = (T)query.setParameter(colName.toUpperCase(), primaryKey).getSingleResult();
        return entity;
    }

    public FlushModeType getFlushMode() {
        checkClosed();
        return FlushModeType.AUTO;
    }

    public <T> T getReference(Class<T> entityClass, Object primaryKey) {
        checkClosed();
        return find(entityClass, primaryKey);
    }

    public void lock(Object entity, LockModeType lockMode) {
        checkClosed();
        throw new UnsupportedOperationException("EntityManager.lock(Object entity, LockModeType lockMode).");
    }

    // implement by creating auto update sql
    public <T> T merge(T entity) {
        checkClosed();
        //TODO: check Version
        checkVersion(entity);
        Query query = new SQLQuery(this, GeneratedNamedSQLTemplate.createUpdateNamedSQLTemplate(entity.getClass()));
        query.setParameter(SQLGenerator.getTableName(entity.getClass()), entity).executeUpdate();
        return entity;
    }

    // implement by creating auto insert sql
    public void persist(Object entity) {
        Query query = new SQLQuery(this, GeneratedNamedSQLTemplate.createInsertNamedSQLTemplate(entity.getClass()));
        query.setParameter(SQLGenerator.getTableName(entity.getClass()), entity).executeUpdate();
    }

    // implement by creating auto select sql
    public void refresh(Object entity) {
        checkClosed();
        Object newEntity = reFetch(entity);
        // update oldEntity use newEntity
        EntityFactory.updateEntityObject(entity, newEntity);
    }

    private Object reFetch(Object entity){
        checkClosed();
        Query query = new SQLQuery(this, GeneratedNamedSQLTemplate.createSelectNamedSQLTemplate(entity.getClass()));
        ColumnMetadata columnMetadata = EntityRepository.getInstance().getPKColumnMetadata(entity.getClass());
        String colName = columnMetadata.getName();
        Object value = columnMetadata.getFieldValue(entity);
        Object newEntity = query.setParameter(colName.toUpperCase(), value).getSingleResult();
        return newEntity;
    }

    // implement by creating auto delete sql
    public void remove(Object entity) {
        checkClosed();
        Query query = new SQLQuery(this, GeneratedNamedSQLTemplate.createDeleteNamedSQLTemplate(entity.getClass()));
        ColumnMetadata columnMetadata = EntityRepository.getInstance().getPKColumnMetadata(entity.getClass());
        String colName = columnMetadata.getName();
        Object value = columnMetadata.getFieldValue(entity);
        query.setParameter(colName.toUpperCase(), value).executeUpdate();
        query.executeUpdate();
    }

    public void setFlushMode(FlushModeType flushMode) {
        log.warn("Can not change FlushMode.");
    }


    /**
     * check version according @Version, increase if successfully
     * @param entity
     * @throws OptimisticLockException
     */
    protected void checkVersion(Object entity) throws OptimisticLockException {
        ColumnMetadata versionColumnMetadata = EntityRepository.getInstance().getVersionColumnMetadata(entity.getClass());
        if(versionColumnMetadata != null) {
            Object version = versionColumnMetadata.getFieldValue(entity);
            Object newEntity = reFetch(entity); // re-fetch from db
            Object newVersion = versionColumnMetadata.getFieldValue(newEntity);

            if(versionColumnMetadata.getFieldType().equals(Timestamp.class)) {
                if(!((Timestamp)version).equals((Timestamp)newVersion)){
                    throw new OptimisticLockException("Entity is: " + entity + ", the last version in db is " + newVersion + ", current version is: " + version);
                }
                else {
                    ((Timestamp)version).setTime(System.currentTimeMillis());
                }
            }
            else {
                if(Long.parseLong(version.toString()) != Long.parseLong(newVersion.toString())) {
                    throw new OptimisticLockException("Entity is: " + entity + ", the last version in db is " + newVersion + ", current version is: " + version);
                }
                else {
                    long v = Long.parseLong(version.toString());
                    v++;
                    versionColumnMetadata.setFieldValue(entity, v);
                }
            }
        }
    }
}
